﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Audio;

namespace XGame.Display
{


    /// <summary>
    /// The Handler class is the glue for XGame.
    /// It is responsible for handling all various forms of input and output,
    /// all content management, screen management, and lastly, camera management.
    /// </summary>
    public static class Handler
    {

        private static Stack<XScreen> iScreens;
        private static Boolean iExitOnEmpty;
        private static GraphicsDeviceManager iGraphics;
        private static SpriteBatch iSpriteBatch;
        private static Input.XMouse iMouse;
        private static Input.XKeyboard iKeyboard;
        private static XCamera iCamera;
        private static Boolean iCallApplyChanges;
        private static Game iGame;
        private static XGameSettings iSettings = XGameSettings.Default;


        /// <summary>
        /// The sprite manager allows for single instance storage of sfx in memory
        /// </summary>
        private static Content.Manager<SoundEffect> iEffectManager;

        /// <summary>
        /// The sprite manager allows for single instance storage of sprites in memory
        /// </summary>
        private static Content.Manager<Texture2D> iSpriteManager;

        /// <summary>
        /// The font manager allows for single instance storage of SpriteFonts in memory
        /// </summary>
        private static Content.Manager<SpriteFont> iFontManager;

        /// <summary>
        /// The song manager allows for single instance storage of songs in memory
        /// </summary>
        private static Content.Manager<Song> iSongManager;



        /// <summary>
        /// The settings object dictates various options that the Handler class must obey.
        /// </summary>
        public static XGameSettings Settings
        {
            get { return iSettings; }
            set { iCallApplyChanges = !ReferenceEquals(iSettings, value); iSettings = value; }
        }



        /// <summary>
        /// Depending on whether or not it is deemed necessary
        /// </summary>
        public static event ExitGame OnExitGame;
        public delegate void ExitGame(Object sender, EventArgs e);



        /// <summary>
        /// The Initialize method of the Handler class takes care of creating all the initial objects that XGame will use
        /// throughout the course of the game.
        /// </summary>
        /// <param name="aDevice">The GraphicsDevice that draws content to the screen</param>
        /// <param name="aExitOnEmpty">A boolean flag that indicates whether or not XGame will raise the OnExitGame event when there are no screens present</param>
        /// <param name="aGame">The Game object</param>
        /// <param name="aSettings">An optional settings object that</param>
        public static void Initialize(Game aGame, GraphicsDeviceManager aManager, Boolean aExitOnEmpty, XGameSettings? aSettings)
        {
            
            // Initialize variables.
            iScreens = new Stack<XScreen>();
            iMouse = new Input.XMouse();
            iKeyboard = new Input.XKeyboard();
            iCamera = new XCamera();
            iExitOnEmpty = aExitOnEmpty;
            iGraphics = aManager;
            iSpriteBatch = new SpriteBatch(iGraphics.GraphicsDevice);
            Settings = aSettings ?? XGameSettings.Default;
            aGame.Content.RootDirectory = "Content";
            iGame = aGame;

            // Create Managers.
            iEffectManager = new Content.Manager<SoundEffect>("SFX", aGame.Content);
            iSpriteManager = new Content.Manager<Texture2D>("Sprites", aGame.Content);
            iFontManager = new Content.Manager<SpriteFont>("Fonts", aGame.Content);
            iSongManager = new Content.Manager<Song>("Music", aGame.Content);

            // Initialize input
            iMouse.Initialize();
            iKeyboard.Initialize();

            // Now, flip 
            iCallApplyChanges = true;

        }

        /// <summary>
        /// The update routine will allow an update to flow through the currently loaded screens
        /// </summary>
        /// <param name="aTime">The current timing state of the game</param>
        public static void Update(GameTime aTime)
        {

            // Check the update inactive flag. If it's true, we can update while inactive.
            // Otherwise, allow it to process if the game is active
            if ((iGame != null) && ((!iGame.IsActive && Settings.UpdateWhileInactive) || iGame.IsActive))
            {

                // Check first for Settings changes
                if (iCallApplyChanges) fApplyChanges();

                // Update the input system
                iMouse.Update(aTime);
                iKeyboard.Update(aTime);

                // Create an EventArgs object to hold the events of the mouse and keyboard
                Input.EventArgs.XInputEventArgs oInputArgs = new Input.EventArgs.XInputEventArgs
                {
                    // Mouse = iMouse.Events,
                    Mouse = new Input.EventArgs.XInputEventArgs.MouseStructure
                    {
                        MouseDowns = iMouse.Events.Where(oKeys => typeof(Input.EventArgs.MouseDownEventArgs) == oKeys.GetType()).Cast<Input.EventArgs.MouseDownEventArgs>(),
                        MouseHelds = iMouse.Events.Where(oKeys => typeof(Input.EventArgs.MouseHeldEventArgs) == oKeys.GetType()).Cast<Input.EventArgs.MouseHeldEventArgs>(),
                        MouseMoved = iMouse.Events.FirstOrDefault(oKeys => typeof(Input.EventArgs.MouseMoveEventArgs) == oKeys.GetType()) as Input.EventArgs.MouseMoveEventArgs,
                        MouseUps = iMouse.Events.Where(oKeys => typeof(Input.EventArgs.MouseUpEventArgs) == oKeys.GetType()).Cast<Input.EventArgs.MouseUpEventArgs>(),
                        ScrollWheel = iMouse.Events.FirstOrDefault(oKeys => typeof(Input.EventArgs.MouseScrollWheelEventArgs) == oKeys.GetType()) as Input.EventArgs.MouseScrollWheelEventArgs
                    },
                    Pressed = iKeyboard.Events.First(oKeys => typeof(Input.EventArgs.KeysPressedEventArgs) == oKeys.GetType()) as Input.EventArgs.KeysPressedEventArgs,
                    Released = iKeyboard.Events.First(oKeys => typeof(Input.EventArgs.KeysReleasedEventArgs) == oKeys.GetType()) as Input.EventArgs.KeysReleasedEventArgs
                };

                // Let's code an additional update to check if we are constrained.
                if (Settings.ConstrainCursor && oInputArgs.Mouse.MouseMoved != null)
                {

                    // Grab the point
                    Point oCursorLocation = oInputArgs.Mouse.MouseMoved.Location;

                    // Apply the constraint
                    if(oCursorLocation.X != (Int32)MathHelper.Clamp(oCursorLocation.X, 0, Settings.Resolution.X) ||
                        oCursorLocation.Y != (Int32)MathHelper.Clamp(oCursorLocation.Y, 0, Settings.Resolution.Y))
                    {
                        Input.EventArgs.XInputEventArgs.MouseStructure oMouse = oInputArgs.Mouse;
                        oMouse.MouseMoved = null;
                        oInputArgs.Mouse = oMouse;
                    }

                }

                // Do a screen system check.
                fUpdateScreenStack();

                // enter a screen updating routine.
                fUpdateScreens(aTime, oInputArgs);

                // Check for no screens; raise event to exit.
                // Or check for the exit key being pressed with exit on empty as false, and no screens available
                if
                    (
                        (iExitOnEmpty && !iScreens.Any()) ||
                        (!iExitOnEmpty && !iScreens.Any() && iKeyboard.IsKeyPressed(Settings.ExitKey))
                    )
                    if (OnExitGame != null) OnExitGame(null, EventArgs.Empty);

            }

        }

        /// <summary>
        /// The draw routine, similar to the update routine, will allow a draw command to flow through the currently loaded screens
        /// </summary>
        /// <param name="aBatch">The SpriteBatch object which allows the screens to draw content write to the display</param>
        public static void Draw()
        {

            // Just like in the update event, check the flags to see if drawing is possible
            if ((iGame != null) && ((!iGame.IsActive && Settings.DrawWhileInactive) || iGame.IsActive))
            {

                // Perform a clear
                iGraphics.GraphicsDevice.Clear(Settings.ClearColor);

                // Start Drawing
                iSpriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, null, null, iCamera.GetTransform() * Matrix.CreateScale(fGetScreenScale()));

                // Pass through to the draw screens handler
                fDrawScreens(iSpriteBatch);

                // All done.
                iSpriteBatch.End();

            }

        }

        /// <summary>
        /// Pushes a new screen on top of the stack and automatically initializes it
        /// </summary>
        /// <param name="aScreen">The already initialized screen to push onto the stack</param>
        public static void Push(XScreen aScreen)
        {

            // Automatically initialize the screen and push it onto the stack
            Push(aScreen, true);

        }

        /// <summary>
        /// Pushes a new screen on top of the stack
        /// </summary>
        /// <param name="aScreen">The already initialized screen to push onto the stack</param>
        /// <param name="aInitalize">If true, the screen will initialize itself</param>
        public static void Push(XScreen aScreen, Boolean aInitalize)
        {

            // Check to see if it needs to be initialized
            if (aInitalize) aScreen.Initialize();

            // Push
            iScreens.Push(aScreen);

        }

        /// <summary>
        /// Pops the newest screen off of the stack and returns it
        /// </summary>
        /// <returns>An IScreen object</returns>
        public static XScreen Pop()
        {

            // Pops a screen off the stack and removes it
            XScreen oReturn = null;
            if(iScreens.Any()) oReturn = iScreens.Pop();
            return oReturn;
        }



        /// <summary>
        /// Provides a simple, easy to access method to return any content a screen might need to load
        /// </summary>
        /// <typeparam name="T">The type of content to get</typeparam>
        /// <param name="aContentName">The string name of the content</param>
        /// <param name="aNotFound">The type to return if the content is not found</param>
        /// <returns></returns>
        public static T GetContent<T>(String aContentName, T aNotFound = default(T))
        {

            // A list of supported content types to return
            var oSupportedTypes = new[] { typeof(SoundEffect), typeof(Song), typeof(Texture2D), typeof(SpriteFont) };

            // Set a variable for the return type
            T oReturnVariable = default(T);

            // Check to see if the type parameter passed is a valid type
            if (Array.IndexOf(oSupportedTypes, typeof(T)) > -1)
            {

                // It's valid; go ahead and use some if logic
                try
                {
                    if (typeof(T) == typeof(SoundEffect))
                        oReturnVariable = (T)Convert.ChangeType(iEffectManager.Get(aContentName), typeof(T));
                    if (typeof(T) == typeof(Song))
                        oReturnVariable = (T)Convert.ChangeType(iSongManager.Get(aContentName), typeof(T));
                    if (typeof(T) == typeof(Texture2D))
                        oReturnVariable = (T)Convert.ChangeType(iSpriteManager.Get(aContentName), typeof(T));
                    if (typeof(T) == typeof(SpriteFont))
                        oReturnVariable = (T)Convert.ChangeType(iFontManager.Get(aContentName), typeof(T));
                }
                catch (Exception)
                {
                    if (aNotFound == null)
                        throw;
                    else
                        oReturnVariable = aNotFound;
                }
                
            }
            
            // All done, return it
            return oReturnVariable;

        }

        /// <summary>
        /// Provides a simple, easy to access method to load any content into the internal content managers
        /// </summary>
        /// <typeparam name="T">The type of content to load</typeparam>
        /// <param name="aContentName">The string name of the content</param>
        public static void LoadContent<T>(String aContentName)
        {

            // A list of supported content types to return
            var oSupportedTypes = new[] { typeof(SoundEffect), typeof(Song), typeof(Texture2D), typeof(SpriteFont) };
            
            // Check to see if the type parameter passed is a valid type
            if (Array.IndexOf(oSupportedTypes, typeof(T)) > -1)
            {

                // It's valid; go ahead and use some if logic
                try
                {
                    if (typeof(T) == typeof(SoundEffect))
                        iEffectManager.Load(aContentName);
                    if (typeof(T) == typeof(Song))
                        iSongManager.Load(aContentName);
                    if (typeof(T) == typeof(Texture2D))
                        iSpriteManager.Load(aContentName);
                    if (typeof(T) == typeof(SpriteFont))
                        iFontManager.Load(aContentName);
                }
                catch (Exception)
                {
                    throw;
                }

            }

        }

        /// <summary>
        /// Adjusts the viewport camera by the given 2D Vector object
        /// </summary>
        /// <param name="aOffset">The offset Vector to apply</param>
        public static void AdjustCamera(Vector2 aOffset)
        {
            iCamera.Move(aOffset);
        }

        /// <summary>
        /// Resets the Viewport camera to its default state
        /// </summary>
        public static void ResetCamera()
        {
            iCamera.Reset();
        }

        /// <summary>
        /// Re-applies all settings for XGame based on the current state of the Settings property
        /// </summary>
        private static void fApplyChanges()
        {

            // Apply game & graphic settings first
            iGraphics.PreferredBackBufferWidth = (Int32)Settings.Resolution.X;
            iGraphics.PreferredBackBufferHeight = (Int32)Settings.Resolution.Y;
            iGraphics.IsFullScreen = Settings.FullScreen;
            iGame.IsMouseVisible = Settings.ShowCursor;
            iGraphics.ApplyChanges();

            // Now apply the input settings
            iKeyboard.Duration = Settings.KeyDelay;
            iMouse.Duration = Settings.MouseDelay;

            // Flip the bit
            iCallApplyChanges = false;

        }


        
        private static void fUpdateScreenStack()
        {

            // Check first to see if there
            // are any screens on the stack
            if (iScreens.Any())
            {

                // Convert the stack to a List<IScreen>
                List<XScreen> oLocalScreens = new List<XScreen>(iScreens.AsEnumerable<XScreen>().Reverse());

                // Now, loop backwards through the screens and remove them if necessary
                for(Int32 nLoopCount = oLocalScreens.Count() - 1; nLoopCount > -1; nLoopCount--)
                {

                    // Grab the current screen and see if we need to remove it!
                    if (oLocalScreens[nLoopCount].Close) oLocalScreens.RemoveAt(nLoopCount);

                }

                // Now then, convert the List back to a Stack and be done
                iScreens = new Stack<XScreen>(oLocalScreens.AsEnumerable<XScreen>());
                

            }

        }
        private static void fUpdateScreens(GameTime aTime, Input.EventArgs.XInputEventArgs aEventArgs)
        {

            // Check for any screens
            if (iScreens.Any())
            {

                // Placeholder variable
                XScreen oCurrentScreen = default(XScreen);

                // Pop the screen off
                oCurrentScreen = Pop();

                // Check to see that something actually popped off the stack
                if (oCurrentScreen != null)
                {

                    // Does this screen allow for previous ones to be updated?
                    // This is also where the input arguments are removed if
                    // the EventsPropigate flag is set to false.
                    if (oCurrentScreen.Settings.UpdatePrevious) fUpdateScreens
                    (
                        aTime,
                        oCurrentScreen.Settings.EventsPropigate ?
                        aEventArgs : new Input.EventArgs.XInputEventArgs()
                    );

                    // Now update the screen
                    oCurrentScreen.Update(aTime, aEventArgs);

                    // Push it back on
                    Push(oCurrentScreen, false);

                }

            }

        }
        private static void fDrawScreens(SpriteBatch aBatch)
        {

            // Check for any screens
            if (iScreens.Any())
            {

                // Placeholder variable
                XScreen oCurrentScreen = default(XScreen);

                // Pop the screen off
                oCurrentScreen = Pop();

                // Check to see that something actually popped off the stack
                if (oCurrentScreen != null)
                {

                    // Do we need to draw any of the underlying screens?
                    if (oCurrentScreen.Settings.DrawPrevious) fDrawScreens(aBatch);

                    // Now draw the current screen
                    oCurrentScreen.Draw(aBatch);

                    // Push it back on
                    Push(oCurrentScreen, false);

                }
                
            }

        }
        private static Vector3 fGetScreenScale()
        {
            var nScaleX = (float)iGraphics.GraphicsDevice.Viewport.Width / (float)Settings.Resolution.X;
            var nScaleY = (float)iGraphics.GraphicsDevice.Viewport.Height / (float)Settings.Resolution.Y;
            return new Vector3(nScaleX, nScaleY, 1.0f);
        }

    }

}
